package server;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.Socket;

import util.Util;
import core.Game;
import core.GameParticipantRegistry;

/**
 * The GameServer plays the role of the Subject in the Observer pattern;
 * it is responsible for maintaining a single Game instance. Communicates with
 * Clients through the ClientProxy class (GoF Proxy pattern).
 */
public class GameServer implements Runnable, core.GameParticipant 
{
	/**
	 * Point-of-entry for GameServer execution.<br>
	 * Usage: java server.GameServer <i>masterServerAddress masterServerPort localGamePort module numPlayers [option1=value1 ...]</i>
	 * @param args
	 */
	public static void main(String[] args)
	{
		try
		{
			int gamePort = Integer.parseInt(args[2]);
			String module = args[3];
			int numPlayers = Integer.parseInt(args[4]);
			java.util.Map<String, String> options = new java.util.HashMap<String, String>();
			util.Pair<String, String> option;
			for (int i = 5; i < args.length; ++i)
				options.put((option = Util.splitOnFirstOccurrence(args[i], '=')).first, option.second);

			java.net.InetAddress host = java.net.InetAddress.getByName(args[0]);
			int port = Integer.parseInt(args[1]);
			System.out.println("Connecting to master server at " + host + ":" + port);
			Socket masterServerConnection = new Socket(java.net.InetAddress.getByName(args[0]), Integer.parseInt(args[1]));

			new Thread(new GameServer(masterServerConnection, gamePort, module, numPlayers, options)).start();

			// tell the master server where we are, what game we're playing, etc.
			PrintWriter out = new PrintWriter(masterServerConnection.getOutputStream());
			out.println(gamePort);
			out.println(module);
			out.println(numPlayers);
			for (String key : options.keySet())
				out.println(key + "=" + options.get(key));
			out.println();
			out.flush();
		} catch (ArrayIndexOutOfBoundsException e) {
			Util.quitWithError(USAGE_STRING, 1);
		} catch (NumberFormatException e) {
			Util.quitWithError(USAGE_STRING, 1);
		} catch (java.net.UnknownHostException e) {
			Util.quitWithError("Unknown host: " + args[0], 3);
		} catch (Exception e) {
			e.printStackTrace();
			Util.quitWithError("An unexpected exception occurred in GameServer: " + e.getMessage(), 2);
		}
	}

	private static final String USAGE_STRING = "Usage: java server.GameServer masterServerAddress masterServerPort localGamePort module numPlayers [option1=value1 ...]";

	/**
	 * Create a new GameServer to interface with a MasterServer and run the given game.
	 * @param masterServerConnection the socket connection to the MasterServer
	 * @param gamePort the port to accept Client connections on
	 * @param module the path to the module file that specifies the game
	 * @param numPlayers the number of players to play the specified game with
	 * @param options a list of server-side options to customize the game specified by the module
	 * @throws IOException
	 */
	public GameServer(Socket masterServerConnection, int gamePort, String module, 
			int numPlayers, java.util.Map<String, String> options) throws IOException
	{
		core.GameParticipant old = GameParticipantRegistry.get();
		GameParticipantRegistry.put(this);
		this.module = module;
		arbiter = DEFAULT_ARBITER;
		gameParser = new module.GameParser(module);
		game = gameParser.parse(numPlayers, options);
		votingManager = new voting.VotingManager(this);
		socket = new java.net.ServerSocket(gamePort);
		this.masterServerConnection = masterServerConnection;
		GameParticipantRegistry.put(old);
	}

	/**
	 * Waits for the requisite number of Clients to connect, then initializes the
	 * state of the game and starts ClientProxy threads to communicate with the 
	 * Clients.
	 */
	public void run()
	{
		GameParticipantRegistry.put(this);
		System.out.println("Accepting clients on port " + socket.getLocalPort());
		try {
			// accept connections from the proper number of clients
			// TODO: listen for all connections at once in threads?
			for (core.Player player : game.getPlayers()) {
				Socket connection = socket.accept();
				PrintWriter out = new PrintWriter(connection.getOutputStream(), true);
				out.println(player.getName());
				clientProxies.add(new ClientProxy(this, player, connection));
				votingManager.addPlayer(player);
			}

			masterServerConnection.close();

			// do any necessary game initialization
			for (commands.Command c : new module.GameObjectInitializer<Game>(getModule(), "server").init(game))
				executeCommand(c);

			// finally, start the ClientProxy threads
			for (ClientProxy cp : clientProxies)
				new Thread(cp).start();
		}
		catch (IOException e) {e.printStackTrace();}
		catch (commands.CommandException e) {e.printStackTrace();}
	}

	/**
	 * Executes a given Command (if it is approved by the Arbiter), and notifies all ClientProxies 
	 * via the <code>ClientProxy.sendStateUpdate</code> method.
	 * @param command The command to execute.
	 * @param account The account from which the command was sent
	 */
	public synchronized void executeCommand(commands.Command command) throws commands.CommandException
	{
		System.out.println("GameServer.executeCommand(" + command + ")");
		
		if (command == null || !arbiter.authorize(command))
			return;

		net.StateUpdate stateUpdate;
		if (!(command instanceof commands.UndoCommand)) {
			if ((stateUpdate = command.execute()) != null && !stateUpdate.isEmpty())
				undoStack.push(stateUpdate.getInverse());
		}
		else
			stateUpdate = undoStack.pop();

		if (stateUpdate != null) {
			stateUpdate.execute();

			for (ClientProxy cp : clientProxies)
				try {
					cp.sendStateUpdate(stateUpdate, command);
				} catch (IOException e) {
					// not entirely sure what to do in this event...
					e.printStackTrace();
				}
		}
	}

	public java.util.List<ClientProxy> getClients() {
		return java.util.Collections.unmodifiableList(clientProxies);
	}
	
	public String getModule() {
		return module;
	}

	public void notifyOfVote(core.Player player, int vote) {
		votingManager.tallyVote(player, vote);
	}
	
	public void notifyOfMeasure(voting.Measure m) {
		if (!votingManager.isActive())
			votingManager.setCurrentMeasure(m);

		//TODO: Need to send message to client that vote suggestion failed
		System.out.println("NOTIFY_OF_VOTE " + m);
	}
	
	public void endGame(String winner) {
	}

	public core.Player getPlayer() {
		return null;
	}

	public Game getGame() {
		return game;
	}	

	private final java.util.Stack<net.StateUpdate> undoStack = new java.util.Stack<net.StateUpdate>();
	/** Proxies for the Clients connected to the GameServer */
	private final java.util.List<ClientProxy> clientProxies = new java.util.LinkedList<ClientProxy>();

	/** The GameParser responsible for instantiating the master copy of the Game */
	private final module.GameParser gameParser;
	/** The server socket for accepting Clients */
	private final java.net.ServerSocket socket;
	/** The master copy of the Game state */
	private Game game;
	private final String module;

	private final Socket masterServerConnection;
	private final voting.VotingManager votingManager;
	private final Arbiter arbiter;

	private static final Arbiter DEFAULT_ARBITER = new Arbiter();
}